//Extension accepts the following XML format:
//
// <root title="profile name">
//   <entry key="english phrase"   kanji="character" />
//   <entry key="english phrase 2" kanji="character" />
//   ...
// </root>

// kanjilish - global extension object - contains and oversees all the global settings.
var kanjilish = {

  profiles: [],   // array of profile information
  current:  0,    // index of the currently active profile

  prefs: null, // Mozilla preferences service - used to store the current profile index
  tabs:  [],   // array of all the 'kanjilish_tab' objects

  key: "R",
  mod: "alt",

  // extension initialization:
  init: function()
  {
    // setup preferences object:
    var prefservice  = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefService);
    this.prefs = prefservice.getBranch("kanjilish.");

	// retrieve (if present) or create (if not present) current profile preference:
	try
	{
		this.current = this.prefs.getIntPref("current");
	}
	catch(e)
	{
		this.prefs.setIntPref("current", 0);
	}

	// retrieve (if present) or create (if not present) shortcut key preference:
	try
	{
		this.key = this.prefs.getCharPref("key");
	}
	catch(e)
	{
		this.prefs.setCharPref("key", "R");
	}

	// retrieve (if present) or create (if not present) shortcut key preference:
	try
	{
		this.mod = this.prefs.getCharPref("mod");
	}
	catch(e)
	{
		this.prefs.setCharPref("mod", "alt");
	}

    // setup unicode converter - used later to load unicode characters from XML files:
    var suniconvCID = "@mozilla.org/intl/scriptableunicodeconverter";
    var suniconvIID = Components.interfaces.nsIScriptableUnicodeConverter;

    var uniConv = Components.classes[suniconvCID].createInstance(suniconvIID);
    uniConv.charset = "UTF-8";

	// create the popup-menu (used later - populated with the list of profiles) in the statusbar UI:
	var submenu = document.createElement("menupopup");
	document.getElementById("kanjilish_sub").appendChild(submenu);

	// extension id:
    var id = "kanjilish@jay.starkey";

	// extension installation folder:
    var ext = Components.classes["@mozilla.org/extensions/manager;1"]
                    .getService(Components.interfaces.nsIExtensionManager)
                    .getInstallLocation(id)
                    .getItemLocation(id); 

	// list all XML files in the installation folder:
    var entries = ext.directoryEntries;
    var files = [];

    while(entries.hasMoreElements())
    {
      var entry = entries.getNext();
      entry.QueryInterface(Components.interfaces.nsIFile);

	  var n = entry.leafName;
      if(n.substr(n.length - 4) == ".xml") files.push(entry);
    }

	// Cycle through the files and try to load them:
    for(var i = 0; i < files.length; i++)
    {
      var file = files[i];

      // open an input stream from file
      var istream = Components.classes["@mozilla.org/network/file-input-stream;1"]
                              .createInstance(Components.interfaces.nsIFileInputStream);
      istream.init(file, 0x01, 0444, 0);
      istream.QueryInterface(Components.interfaces.nsILineInputStream);

      // read lines into array
      var line = {}, lines = [], hasmore;
      do
      {
        hasmore = istream.readLine(line);
        lines.push(line.value); 
      }
      while(hasmore);

      istream.close();

      try
      {
	    // XML file loaded into a single string:
        var xml = lines.join("");
        xml = xml.substr(xml.indexOf("<"));

		// parsed XML DOM tree:
        var dom = (new DOMParser()).parseFromString(xml, "text/xml");
        var name = dom.documentElement.getAttribute("title");
		if(!name) continue; // no title = wrong format

		// Already loaded?
		for(var k = 0; k < this.profiles; k++)
		{
		  if(this.profiles[k][0] == name)
		    continue; // yes - proceed to the next file
		}

		// Accept the profile:
        var index = this.profiles.length;
        this.profiles[index] = [name, []];

		// Load all the entries of the profile:
        var elements = dom.documentElement.childNodes;
        for(var j = 0; j < elements.length; j++)
        {
          var element = elements[j];
          this.profiles[index][1][j] = [element.getAttribute("key").toLowerCase(), element.getAttribute("kanji").split(";")];

          // convert characters to Unicode:
          var array = this.profiles[index][1][j][1];
          for(var k = 0; k < array.length; k++)
            array[k] = uniConv.ConvertToUnicode(array[k]);
        }

        // Loading complete. Sort the keys by length:
        this.profiles[index][1].sort(function(a, b)
        {
          if(a[0].length < b[0].length)
            return 1;

          if(a[0].length > b[0].length)
            return -1;

          return 0;
        });

		// Update the profile popup-menu:
		this.update();
      }
      catch(error) { alert(error); }
    }

	// Create the 'kanjilish_tab' object for the first tab:
	this.tabs[0] = new kanjilish_tab(gBrowser.selectedBrowser);

	// Setup tab event handlers:
    var container = gBrowser.tabContainer;
    container.addEventListener("TabOpen",   this.onTabAdded,    false);
    container.addEventListener("TabSelect", this.onTabSelected, false);
    container.addEventListener("TabClose",  this.onTabClosed,   false);
  },

  // Correctly deinstall all event handlers at browser shut-down:
  uninit: function()
  {
    var container = gBrowser.tabContainer;
    container.removeEventListener("TabOpen",   this.onTabAdded,    false);
    container.removeEventListener("TabSelect", this.onTabSelected, false);
    container.removeEventListener("TabClose",  this.onTabClosed,   false);

	// Some garbage collection:
    for(var i = 0; i < this.tabs.length; i++)
	  this.tabs[i].uninit();
  },

  // On adding new tab - create a 'kanjilish_tab' for this new tab:
  onTabAdded: function(event)
  {
	kanjilish.tabs[kanjilish.tabs.length] = new kanjilish_tab(event.target.linkedBrowser);

	// New tabs default to "off":
	document.getElementById("kanjilish-check").setAttribute("checked", "false");
  },

  // On switching tabs - check if the 'kanjilish_tab' is active on this tab:
  onTabSelected: function(event)
  {
	kanjilish.updateCheck(event.target.linkedBrowser);
  },

  // On tab destruction - check if the 'kanjilish_tab' is active on last selected tab:
  onTabClosed: function(event)
  {
    var browser = event.target.linkedBrowser;
    for(var i = 0; i < kanjilish.tabs.length; i++)
	{
	  if(kanjilish.tabs[i].browser == browser)
	  {
	    kanjilish.tabs[i].uninit();
	    kanjilish.tabs.splice(i, 1);
	  }
	}

	kanjilish.updateCheck(gBrowser.selectedBrowser);
  },

  // Syncronize the global profiles array with the (usually called when new XML files are loaded or deleted in the Options screen):
  update: function()
  {
    var submenu = document.getElementById("kanjilish_sub").firstChild;

	// Delete all profile items in the menu:
    while(submenu.firstChild)
	  submenu.removeChild(submenu.firstChild);

	// Add the profile items:
    for(var i = 0; i < this.profiles.length; i++)
	{
      var menuitem = document.createElement("menuitem");
      submenu.appendChild(menuitem);

      menuitem.setAttribute("type", "checkbox");
      menuitem.setAttribute("autocheck", "false"); // no autocheck
      menuitem.setAttribute("checked", (this.current == i) ? "true" : "false");
      menuitem.setAttribute("label", this.profiles[i][0]);

	  // set up the appropriate event handler:
      menuitem.addEventListener("click", this.onChangeProfile, false);
    }

	var key = document.getElementById("kanjilish-reprocess");
	key.setAttribute("key", this.key);
	key.setAttribute("modifiers", this.mod);

	this.prefs.setCharPref("key", this.key);
	this.prefs.setCharPref("mod", this.mod);

	var re = document.getElementById("kanjilish-re-item");

	var mods = this.mod.split(" ");
	var l1 = mods[0].length;
	var l2 = mods[1] ? mods[1].length : 0;

	if(l1) mods[0] = mods[0].charAt(0).toUpperCase() + mods[0].substr(1);
	if(l2) mods[1] = mods[1].charAt(0).toUpperCase() + mods[1].substr(1);

	re.setAttribute("acceltext", (l1 ? (mods[0] + "+") : "") + (l2 ? (mods[1] + "+") : "") + this.key);
  },

  // Check if the tab (pointed by 'browser' object) has it's 'kanjilish_tab' object switched on and off:
  updateCheck: function(browser)
  {
    if(browser && browser.kanjilish)
	  document.getElementById("kanjilish-check").setAttribute("checked", browser.kanjilish.active ? "true" : "false");
  },

  // Call Options dialog:
  showOptions: function()
  {
    window.openDialog('chrome://kanjilish/content/options.xul', 'KanjilishOptions', 'centerscreen=yes,chrome=yes,modal=yes');
  },

  // process current tab:
  process: function()
  {
    var browser = gBrowser.selectedBrowser; // selected tab's browser object

	// Switch 'kanjilish_tab' state:
    browser.kanjilish.active = !browser.kanjilish.active;

	if(browser.kanjilish.active)
      browser.kanjilish.process(); // process if active
	else
      browser.kanjilish.restore(); // restore if inactive

	// update the statusbar UI:
	this.updateCheck(browser);
  },

  reprocess: function()
  {
    var browser = gBrowser.selectedBrowser; // selected tab's browser object

	if(browser.kanjilish.active)
      browser.kanjilish.process(); // process if active
  },

  // When changing profiles - find the proper profile index, assign it globally and save it to preferences:
  onChangeProfile: function(event)
  {
    var name = event.target.getAttribute("label");
    var menu = document.getElementById("kanjilish_sub").firstChild;

    for(var i = 0; i < kanjilish.profiles.length; i++)
    {
      var menuitem = menu.childNodes[i];

      if(kanjilish.profiles[i][0] == name)
      {
        kanjilish.current = i; // i is the profile index
		menuitem.setAttribute("checked", "true");

		kanjilish.prefs.setIntPref("current", i);
      }
      else menuitem.setAttribute("checked", "false");
    }

	// Restore then process current tab again:
	kanjilish.process();
	kanjilish.process();
  }
}

// 'kanjilish_tab' object - holds per-tab information and handles specific tabs:
function kanjilish_tab(browser)
{
  this.init(browser);
}

kanjilish_tab.prototype = {

  active:   false, // state: "on"/"off" ("off") by default
  browser:  null,  // browser object this 'kanjilish_tab' is bound to
  spans:    [],    // SPAN objects (tags) that hold the "processed" phrases; used for restoring the page to it's original state

  // constructor:
  init: function(browser)
  {
    this.browser = browser;
	this.browser.kanjilish = this; // attach this 'kanjilish_tab' object to the 'browser' object

	this.browser.addEventListener("DOMContentLoaded", this, false);
  },

  uninit: function()
  {
    // proper garbage collection:
	this.browser.removeEventListener("DOMContentLoaded", this, false);
  },

  // handler for DOMContentLoaded:
  handleEvent: function(event)
  {
    // inner function - retrieves 'browser' object by referencing document
	function getBrowserForDocument(doc)
	{
	  var browsers = gBrowser.browsers;

	  for(var i = 0; i < browsers.length; i++)
	  {
		if(doc == browsers[i].contentDocument)
		  return browsers[i];
	  }

	  return null;
	};

	switch(event.type)
	{
	  case "DOMContentLoaded":
	  {
		var htmlDocument = event.originalTarget;
		var browser = getBrowserForDocument(htmlDocument);

		// process the page if kanjilish_tab is active:
		if(browser && browser.kanjilish && (browser.kanjilish.active == true))
		  browser.kanjilish.process();

		break;
	  }
	}
  },

  // main processing procedure:
  process: function(win)
  {
    if(!this.active) return;

    var profile = kanjilish.profiles[kanjilish.current][1];

	// are we in the top frame?
	if(!win)
	{
      var browser = gBrowser.selectedBrowser;
      win = browser.contentWindow;
	}

	// process inner frames, if any:
	var winframes = win.frames;
	if(winframes)
	{
	  for(var i = 0; i < winframes.length; i++)
	    this.process(winframes[i]);
	}

	// '@mozilla.org/embedcomp/rangefind;1' class finds words (to be exact - DOM ranges, containing words) for us:
    var findRange = Components.classes['@mozilla.org/embedcomp/rangefind;1'].createInstance().QueryInterface(Components.interfaces.nsIFind);
    findRange.caseSensitive = false;
    findRange.findBackwards = false;

	var doc = win.document;
	var doctext = doc.body.textContent.toLowerCase();

    var range = doc.createRange();
    range.selectNode(doc.body);

    var stopRange = range.cloneRange();
    stopRange.collapse(false);

    var l = profile.length;

	// cycle through all the profile keys:
    for(var i = 0; i < l; i++)
    {
	  var prf = profile[i];
      var key = prf[0];

	  // does the match exists (this is much faster than just starting the rangefind):
	  if(doctext.indexOf(key) == -1) continue;

      // Reset the start range:
      var startRange = range.cloneRange();
      startRange.collapse(true);

	  // start finding:
      var result = findRange.Find(key, range, startRange, stopRange);

      while(null != result)
      {
		var reStart = result.startContainer;
		var reEnd = result.endContainer;

		if(reEnd && reStart && (reEnd.nodeType == Node.TEXT_NODE) && (reStart.nodeType == Node.TEXT_NODE))
		{
		  var startChar = null;
          if(result.startOffset > 0)
            startChar = reStart.nodeValue.charAt(result.startOffset - 1);

          var endChar = reEnd.nodeValue.charAt(result.endOffset);

		  // did we find an entire word?
          if(!this._is_invalid_character(startChar) && !this._is_invalid_character(endChar) && this._is_valid_span(reStart.parentNode))
          {
		    // yes:
            startRange.selectNode(this._highlight(result, prf[1]));
          }
		  else startRange = result;
		}
		else startRange = result;

		// next match:
        startRange.collapse(false);
        result = findRange.Find(key, range, startRange, stopRange);
      }
    }
  },

  // given the DOM range and the array of (possible) symbols, substitute 1st character in the range with one of the given symbols:
  _highlight: function(range, symbols)
  {
    var startContainer = range.startContainer;
	var num = this.spans.length;

	// wrap range in a SPAN object - so we don't match this key/range later:
    var spanNode = startContainer.ownerDocument.createElement("span");
    spanNode.setAttribute("id", "text-replace-span-" + num);

    var docfrag = range.extractContents();
	var child = docfrag.childNodes[0];
	var text = child.textContent;

	// what symbol to use:
	var symbol = (symbols.length == 1) ? symbols[0] : symbols[Math.floor(Math.random() * symbols.length)];

	// array of spans and original span values - used while restoring the page to it's original state:
	this.spans[num] = [spanNode, text];

	child.textContent = symbol + text.substr(1);

    spanNode.appendChild(docfrag);
    range.insertNode(spanNode);

	return spanNode;
  },

  // support function - tells if the input character is a word-breaking character.
  // It is used to define if we've met an entire word or partial match (we need entire words only).
  _is_invalid_character: function(ch)
  {
	if(!ch || (typeof ch != "string" || ch.length != 1))
	  return false;

	var code = ch.charCodeAt(0);
	
	// this is A-Za-z0-9 and apostrophes
	return ((code >= 48 && code <= 57) || (code >= 65 && code <= 90)  ||
	        (code >= 97 && code <= 122) || (code >= 138 && code <= 142) ||
			(code >= 154 && code <= 159) || (code >= 192 && code <= 255) ||
			(code == 39));
  },

  //support function - tells if the input node is the node that wasn't previously generated by kanjilish:
  _is_valid_span: function(node)
  {
    if(node.id)
	  if(node.id.indexOf("text-replace-span-") != -1)
		return false;

	return true;
  },

  // restore the page to it's original state:
  restore: function()
  {
    for(var i = 0; i < this.spans.length; i++)
	{
	  var obj  = this.spans[i];
	  var span = obj[0];

	  if(span)
	  {
	    // revert to original values:
	    span.textContent = obj[1];
	    span.removeAttribute("id");
      }
	}

	this.spans = [];
  }
};

// Initialize the extension on browser start, deinitialize on shutdown:
window.addEventListener("load",   function() { kanjilish.init();   }, false);
window.addEventListener("unload", function() { kanjilish.uninit(); }, false);